import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;


/**
 * A class used to generate a graph representing all possible
 * routes through the IBMS system. From this class, we can find
 * all the best routes in terms of how long it takes to travel
 * upon this particular Edge. The graph is stored in a hash map,
 * the locationsMap. To access a Location within this map, we use
 * a string concatenation of a location's area name and stop name.
 *
 * <br />We must be aware that the locations generated by the roster are
 * NOT tied together, that is even if two locations have the same ID, they
 * do not represent the same Location reference. Therefore, when a graph is
 * generated it links all these Locations together.
 *
 * <br />The graph generated uses the specified start time to generate it, so the
 * hash map only links nodes for the day of the start time. Thus,
 * changing the start time requires a rebuild of the graph!
 * @author higgina0
 */
public class RouteGenerator
{
    private static final int MINUTES_PER_DAY = 1440;
    
    private HashMap<String, Location> locationsMap;
   //The start time to find services for
    private Calendar startTime;
    //All buses
    private ArrayList<ArrayList<Bus>> buses;

    /**
     * Creates a RouteGenerator object.
     * Upon this object being created, the graph is generated and thus
     * generating this object will take a long time.
     * @param startTime The time to generate the graph for.
     */
    public RouteGenerator(Calendar startTime)
    {
        this.startTime = startTime;
        this.locationsMap = new HashMap<String, Location>();
        Roster r = new Roster();
        this.buses = r.assignBuses();
        generateGraph();
    }

    /**
     * Creates a route generator with a full graph
     * for each day of the week
     */
    public RouteGenerator()
    {
        this.locationsMap = new HashMap<String, Location>();
        Roster r = new Roster();
        this.buses = r.assignBuses();
        generateFullGraph();
    }

    /*
     * Generates the full graph for each day of
     * the week.
     */
    private void generateFullGraph()
    {
        for (int currentDay = 0; currentDay < buses.size(); currentDay++)
        {
            ArrayList<Bus> busesToday = buses.get(currentDay);
            //A list of every service
            ArrayList<Service> services = new ArrayList<Service>();
            ArrayList<BusShift> shiftsToday = new ArrayList<BusShift>();
            Iterator<Bus> busIter = busesToday.iterator();
            //Add all shifts to the list
            while (busIter.hasNext())
            {
                Bus currentBus = busIter.next();
                ArrayList<BusShift> busShifts = currentBus.getShifts();
                Iterator<BusShift> busShiftsIter = busShifts.iterator();

                while (busShiftsIter.hasNext())
                    shiftsToday.add(busShiftsIter.next());
            }

            //Go through each  service
            Iterator<BusShift> shiftIter = shiftsToday.iterator();
            while (shiftIter.hasNext())
                services.add(shiftIter.next().getService());

            //Service first = services.get(0);
            //BusShift firstShift = first.get

            //We now have every service, we need to get the times from this service
            Iterator<Service> serviceIter = services.iterator();
            while (serviceIter.hasNext())
            {
                Service current = serviceIter.next();
                //System.out.println("Current date: " + Service.calendarToDateString(current.getStartTime()));
                ArrayList<Location> locations = current.getBusStops();
                ArrayList<Calendar> times = current.getTimes();
                //We now add a path to every location, except from the last
                for (int index = 0; index < locations.size() - 1; index++)
                {
                     //For each location, set a path to the next bus stop
                    Calendar thisTime = times.get(index);
                    Calendar nextStopTime = times.get(index + 1);
                    int minutesThis = thisTime.get(Calendar.MINUTE) + (thisTime.get(Calendar.HOUR_OF_DAY) * 60);
                    int minutesNext = nextStopTime.get(Calendar.MINUTE) + (nextStopTime.get(Calendar.HOUR_OF_DAY) * 60);
                    int duration = -1;

                    if (minutesThis >= minutesNext)
                        duration = MINUTES_PER_DAY - minutesThis + minutesNext;
                    else
                        duration = minutesNext - minutesThis;
                    //Add the edge to the current location
                    Location currentLoc = locations.get(index);

                    Location next = locations.get(index + 1);

                    //If next is not in hash map, add it
                    String nextHash = next.getStopName() + next.getAreaName();
                    if (!this.locationsMap.containsKey(nextHash))
                    {
                        String hashString = next.getStopName() + next.getAreaName();
                        this.locationsMap.put(hashString, next);

                        //System.out.println("Added: " + next.getStopName() + " to hash map with hash key: " + next.getStopName().hashCode());
                    }

                    //If current is not in hash map, add and update, otherwise just update.
                    String currentHash = currentLoc.getStopName() + currentLoc.getAreaName();
                    if (this.locationsMap.containsKey(currentHash))
                    {
                        String hashString = currentLoc.getStopName() + currentLoc.getAreaName();
                        Location l = this.locationsMap.get(hashString);
                        String hashNext = next.getStopName() + next.getAreaName();
                        Location nextLoc = this.locationsMap.get(hashNext);
                        //System.out.println("For service: " + current.getServiceID());
                        //System.out.println("Adding edge from: " + l.getStopName() + "Id: "+ l.getLocationID() + " to: " + nextLoc.getStopName() + " with duration: " + duration);
                        l.addEdge(current, nextLoc, duration);

                        //System.out.println("Updated: " + currentLoc.getStopName() + " to hash map with hash key: " + currentLoc.getStopName().hashCode());

                    }
                    else
                    {
                        String hashString = currentLoc.getStopName() + currentLoc.getAreaName();
                        this.locationsMap.put(hashString, currentLoc);

                        String hashNext = next.getStopName() + next.getAreaName();
                        Location l = this.locationsMap.get(hashString);
                        Location nextLoc = this.locationsMap.get(hashNext);
                        //System.out.println("Adding edge from: " + l.getStopName() + "Id: "+ l.getLocationID() + " to: " + nextLoc.getStopName() + " with duration: " + duration);
                        l.addEdge(current, nextLoc, duration);

                        //System.out.println("Added: " + currentLoc.getStopName() + " to hash map with hash key" + currentLoc.getStopName().hashCode());
                    }
                }
            }
            System.out.println("Graph built for: " + currentDay);
        }
    }

    /**
     * Generates the graph for this start time.
     * @param buses An array list of buses generated by the roster
     */
    private void generateGraph()
    {
        int currentDay = this.startTime.get(Calendar.DAY_OF_WEEK) - 2;

        //System.out.println("Current day: " + currentDay);
        //int currentDay = 1;
        //System.out.println("Current day: " + Service.dayToString(startTime));
        //All shifts on this current day
        ArrayList<Bus> busesToday = buses.get(currentDay);
        //A list of every service
        ArrayList<Service> services = new ArrayList<Service>();
        ArrayList<BusShift> shiftsToday = new ArrayList<BusShift>();
        Iterator<Bus> busIter = busesToday.iterator();
        //Add all shifts to the list
        while (busIter.hasNext())
        {
            Bus currentBus = busIter.next();
            ArrayList<BusShift> busShifts = currentBus.getShifts();
            Iterator<BusShift> busShiftsIter = busShifts.iterator();

            while (busShiftsIter.hasNext())
                shiftsToday.add(busShiftsIter.next());
        }

        //Go through each  service
        Iterator<BusShift> shiftIter = shiftsToday.iterator();
        while (shiftIter.hasNext())
            services.add(shiftIter.next().getService());

        //Service first = services.get(0);
        //BusShift firstShift = first.get

        //We now have every service, we need to get the times from this service
        Iterator<Service> serviceIter = services.iterator();
        while (serviceIter.hasNext())
        {
            Service current = serviceIter.next();
            //System.out.println("Current date: " + Service.calendarToDateString(current.getStartTime()));
            ArrayList<Location> locations = current.getBusStops();
            ArrayList<Calendar> times = current.getTimes();
            //We now add a path to every location, except from the last
            for (int index = 0; index < locations.size() - 1; index++)
            {
                 //For each location, set a path to the next bus stop
                Calendar thisTime = times.get(index);
                Calendar nextStopTime = times.get(index + 1);
                int minutesThis = thisTime.get(Calendar.MINUTE) + (thisTime.get(Calendar.HOUR_OF_DAY) * 60);
                int minutesNext = nextStopTime.get(Calendar.MINUTE) + (nextStopTime.get(Calendar.HOUR_OF_DAY) * 60);
                int duration = -1;

                if (minutesThis >= minutesNext)
                    duration = MINUTES_PER_DAY - minutesThis + minutesNext;
                else
                    duration = minutesNext - minutesThis;
                //Add the edge to the current location
                Location currentLoc = locations.get(index);

                Location next = locations.get(index + 1);

                //If next is not in hash map, add it
                String nextHash = next.getStopName() + next.getAreaName();
                if (!this.locationsMap.containsKey(nextHash))
                {
                    String hashString = next.getStopName() + next.getAreaName();
                    this.locationsMap.put(hashString, next);

                    //System.out.println("Added: " + next.getStopName() + " to hash map with hash key: " + next.getStopName().hashCode());
                }

                //If current is not in hash map, add and update, otherwise just update.
                String currentHash = currentLoc.getStopName() + currentLoc.getAreaName();
                if (this.locationsMap.containsKey(currentHash))
                {
                    String hashString = currentLoc.getStopName() + currentLoc.getAreaName();
                    Location l = this.locationsMap.get(hashString);
                    String hashNext = next.getStopName() + next.getAreaName();
                    Location nextLoc = this.locationsMap.get(hashNext);
                    //System.out.println("For service: " + current.getServiceID());
                    //System.out.println("Adding edge from: " + l.getStopName() + "Id: "+ l.getLocationID() + " to: " + nextLoc.getStopName() + " with duration: " + duration);
                    l.addEdge(current, nextLoc, duration);

                    //System.out.println("Updated: " + currentLoc.getStopName() + " to hash map with hash key: " + currentLoc.getStopName().hashCode());

                }
                else
                {
                    String hashString = currentLoc.getStopName() + currentLoc.getAreaName();
                    this.locationsMap.put(hashString, currentLoc);

                    String hashNext = next.getStopName() + next.getAreaName();
                    Location l = this.locationsMap.get(hashString);
                    Location nextLoc = this.locationsMap.get(hashNext);
                    //System.out.println("Adding edge from: " + l.getStopName() + "Id: "+ l.getLocationID() + " to: " + nextLoc.getStopName() + " with duration: " + duration);
                    l.addEdge(current, nextLoc, duration);

                    //System.out.println("Added: " + currentLoc.getStopName() + " to hash map with hash key" + currentLoc.getStopName().hashCode());
                }
            }
        }
    }

    /**
     * Finds the best route between two given areas. It does this
     * by calculating the distance from every node in the source area
     * to every node in the target area.
     * @param sourceArea The area name for the source
     * @param targetArea The area name for the target
     * @return An ArrayList of Locations representing the route,
     *         or null if no such route exists.
     */
    public ArrayList<Location> getBestRouteBetweenAreas(String sourceArea, String targetArea)
    {
        ArrayList<Location> startLocations = getLocationsInArea(sourceArea);
        ArrayList<Location> endLocations = getLocationsInArea(targetArea);

        System.out.println("Start locations size: " + startLocations.size() + " end locations: " + endLocations.size());
        ArrayList<Location> bestRoute = null;

        double minDuration = 32768.0;

        for (int i = 0; i < startLocations.size(); i++)
        {
            for (int j = 0; j < endLocations.size(); j++)
            {
                ArrayList<Location> foundRoute = generateBestRoute(startLocations.get(i), endLocations.get(j));
                Iterator<Location> routeIter = foundRoute.iterator();
                double routeDuration = 0;
                while (routeIter.hasNext())
                {
                    Location l = routeIter.next();
                    routeDuration = l.getMinDistance();
                }
                if (routeDuration < minDuration)
                {
                    bestRoute = foundRoute;
                    minDuration = routeDuration;
                }
            }
        }
        return bestRoute;
    }


    /**
     * Find all the locations within a specified area
     * @param areaName The area name to search for locations with
     * @return A list of locations with this area name
     */
    public ArrayList<Location> getLocationsInArea(String areaName)
    {
        ArrayList<Location> allLocations = new ArrayList<Location>(this.locationsMap.values());
        ArrayList<Location> returnLocations = new ArrayList<Location>();
        Iterator<Location> locationIter = allLocations.iterator();
        
        while (locationIter.hasNext())
        {
            Location next = locationIter.next();
            System.out.println("next name: " + next.getAreaName());
            System.out.println("Current name: " + areaName);
            if (next.getAreaName().equalsIgnoreCase(areaName))
                returnLocations.add(next);
        }

        return returnLocations;
    }

    public Location getLocationInGraph(String locationName, String areaName)
    {
        return this.locationsMap.get(locationName + areaName);
    }

    /**
     * Tries to find the best route from the start location to
     * the end location. Returns null if the start and end location
     * have not been set
     * @return A list of locations on which to travel
     */
    public ArrayList<Location> generateBestRoute(Location startLocation, Location endLocation)
    {       
        Location locationStartInHash = this.locationsMap.get((startLocation.getStopName() + startLocation.getAreaName()));
        //System.out.println("Location start in hash: " + locationStartInHash);
        Dijkstra.computeAllPaths(this.locationsMap, locationStartInHash, this.startTime);
        Location locationEndInHash = this.locationsMap.get((endLocation.getStopName() + endLocation.getAreaName()));
        //System.out.println("Location end in hash: " + locationEndInHash);     
        return Dijkstra.getShortestPath(locationEndInHash);
    }

    /*
     * Used for debugging a location. All the information about a location
     * is printed. The location passed is first found in the hash map, and
     * then all edges coming from this location are printed. This takes up
     * a lot of space in the console!
     */
    private ArrayList<Location> printLocationStats(Location location)
    {
        Location l = this.locationsMap.get((location.getStopName() + location.getAreaName()));
        Iterator<Edge> edgeIter = l.getOutlist().iterator();
        ArrayList<Location> returnLocation = new ArrayList<Location>();
        System.out.println("From: " + l.getStopName());
        while (edgeIter.hasNext())
        {
            Edge current = edgeIter.next();
            if (current.getService().getServiceID() == 6527)
                returnLocation  = current.getService().getBusStops();
            System.out.println("Edge: " + current.getEndLocation().getStopName() + " -Area: " + current.getEndLocation().getAreaName() + " -With Service: " + current.getService().getServiceID() + " -and duration: " + current.getDuration());
        }
        
        return returnLocation;
    }

    public HashMap<String, Location> getLocationsMap() {
        return locationsMap;
    }

    /**
     * Sets the start time, which requires a rebuild of the graph
     * for the newly entered start time.
     * @param startTime The start time to set to.
     */
    public void setStartTime(Calendar startTime) {
        this.startTime = startTime;
    }

    public ArrayList<ArrayList<Bus>> getBuses() {
        return buses;
    }

    



    public static void main(String[] args)
    {
        //database.busDatabase.close();
        database.openBusDatabase();
        /*Calendar cal = Calendar.getInstance();
        Location loc = new Location(784);
        Location end = new Location(793);
        System.out.println("For location: " + loc.getStopName());
        System.out.println("To end location: " + end.getStopName());
        System.out.println("\n\n");
        RouteGenerator test = new RouteGenerator(loc, end, cal);
        Roster r = new Roster();
        ArrayList<ArrayList<Bus>> buses = r.assignBuses();
        ArrayList<Location> route = test.generateBestRoute(buses);*/

        ///////////////////////////////////
        //TEST LOCATIONS IN AREA
        ///////////////////////////////////
        /*System.out.println("Testing locations in area!");
        ArrayList<Location> areaLocation = test.getLocationsInArea("Stockport");
        Iterator<Location> areaIter = areaLocation.iterator();

        while (areaIter.hasNext())
        {
            Location next = areaIter.next();
            System.out.println("Location: " + next.getStopName() + " area: " + next.getAreaName());
        }


        Iterator<Location> routeIter = route.iterator();
        System.out.println("Starting from: " + loc.getStopName());*/
        //test.printLocationStats(loc);
        /*int totalDuration = 0;
        while (routeIter.hasNext())
        {
            Location l = routeIter.next();
            System.out.println("---------------------------------------");
            System.out.println("Area: " + l.getAreaName());
            System.out.println("Stop: " + l.getStopName());
            System.out.println("ID: " + l.getLocationID());
            if (l.getPreviousService() != null)
            {
                System.out.println("Service: " + l.getPreviousService().getServiceID());
                Iterator<Location> serviceLocs = l.getPreviousService().getBusStops().iterator();
                System.out.println("---------------------------------------");*/
                /*System.out.println("Bus Stops: ");
                int count = 0;
                ArrayList<Calendar> times = l.getPreviousService().getTimes();
                while (serviceLocs.hasNext())
                {
                    Location next = serviceLocs.next();
                    System.out.println("\t" + next.getStopName() + " " + next.getLocationID() + " Area: " + next.getAreaName() + " Time: " + Service.calendarToString(times.get(count)));
                    count++;
                }
                System.out.println("---------------------------------------");*/
                //System.out.println("Start time: " + Service.calendarToString(l.getPreviousService().getStartTime()) + "Date: " + Service.calendarToDateString(l.getPreviousService().getStartTime()));
                //System.out.println("End time: " + Service.calendarToString(l.getPreviousService().getEndTime()) + "Date: " + Service.calendarToDateString(l.getPreviousService().getEndTime()));
            /*}
            
            if (l.getLatestTime() != null)
                System.out.println("Time: " + Service.calendarToString(l.getLatestTime()));
            System.out.println("Duration: " + l.getMinDistance());
            totalDuration += l.getMinDistance();
            System.out.println("---------------------------------------");
        }
        
        System.out.println("Total duration: " + totalDuration + " minutes");*/
        
        /*System.out.println("Stats: ");
        ArrayList<Location> stops = test.printLocationStats(loc);
        Iterator<Location> stopsIter = stops.iterator();
        System.out.println("\n\n----------------------------------\n\n");
        while (stopsIter.hasNext())
        {
            Location stop = stopsIter.next();
            System.out.println(stop.getStopName() + " " + stop.getAreaName());
        }
        System.out.println("\n\n----------------------------------\n\n");
        System.out.println("Stats for end: ");
        test.printLocationStats(end);
        


        Location testLoc = test.locationsMap.get(loc.getStopName());
        ArrayList<Edge> outlist = testLoc.getOutlist();
        Iterator<Edge> edgeIter = outlist.iterator();*/
        /*while (edgeIter.hasNext())
        {
            Edge e = edgeIter.next();
            Service s = e.getService();
            Location l = e.getEndLocation();
            System.out.println("Service: " + s.getServiceID());
            System.out.println("next location: " + l.getStopName());
        }*/
        //test.printLocationStats(loc);
        //System.out.println(test.locationsMap.toString());

        //System.out.println("Finished!");


        //////////////////////////////////////////
        //TESTING THE GET ALL LOCATIONS IN AREA
        //////////////////////////////////////////
        Calendar cal = Calendar.getInstance();

        RouteGenerator gen = new RouteGenerator(cal);

        String startLocation = "Stockport";
        String endLocation = "Romiley";
        ArrayList<Location> startLocations = gen.getLocationsInArea(startLocation);
        ArrayList<Location> endLocations = gen.getLocationsInArea(endLocation);

        ArrayList<Location> bestRoute;

        double minDuration = 32768.0;

        for (int i = 0; i < startLocations.size(); i++)
        {
            for (int j = 0; j < endLocations.size(); j++)
            {
                ArrayList<Location> foundRoute = gen.generateBestRoute(startLocations.get(i), endLocations.get(j));
                Iterator<Location> routeIter = foundRoute.iterator();
                double routeDuration = 0;
                System.out.println("\n-----------------FOUND A NEW ROUTE------------------");
                while (routeIter.hasNext())
                {
                    Location l = routeIter.next();
                    System.out.println("---------------------------------------");
                    System.out.println("Area: " + l.getAreaName());
                    System.out.println("Stop: " + l.getStopName());
                    if (l.getPreviousService() != null)
                    {
                        System.out.println("Service: " + l.getPreviousService().getServiceID());
                    }

                    if (l.getLatestTime() != null)
                        System.out.println("Time: " + Service.calendarToString(l.getLatestTime()));
                    System.out.println("Duration: " + l.getMinDistance());
                    routeDuration = l.getMinDistance();
                    System.out.println("---------------------------------------");

                }
                if (routeDuration < minDuration)
                {
                    minDuration = routeDuration;
                }
            }
        }

        if (minDuration == 32768.0)
            System.out.println("No smallest route!");
        else
            System.out.println("Smallest route was: " + minDuration);

        database.busDatabase.close();

    }




}
